# -*- encoding: utf-8 -*-

"""
------------------------------------------
@File       : life_game_v6_coroutine.py.py
@Author     : maixiaochai
@Email      : maixiaochai@outlook.com
@CreatedOn  : 2021/7/27 10:37
------------------------------------------
     [ 生命游戏-协程版 ]

协程相关：
    1）协程启动是有代价的，就是必须做一次函数调用
    2）协程所要执行的任务也是用一个函数来表示的，在执行这个函数的过程中，协程可以从执行环境中获取输入值，并把输出结果放到这个执行环境中
    3）协程与线程的区别在于，协程不会把函数从头到尾执行完，而是每遇到一个 await表达式，就暂停一次，
        下次继续的时候，它会先等待 await所针对的那项 awaitable操作有了结果（那项操作是用async函数表示的），然后再推进到下一个 await表达式
    4）Python系统可以让数量极多的async函数各自向前推进，看起来像很多Python线程那样，能够并发地运行
    5）不会占用大量的内存，启动和切换的开销也比较小，而且不用复杂的代码来实现加锁和同步，这种强大的机制是通过事件循环（event loop）打造的

协程优点：
    能够把那些与外部环境交互的代码（例如 I/O调用）与那些实现自身需求的代码（如事件循环）解耦。这让我们可以把重点放在实现需求所用的逻辑上，
    而不用专门花时间去写一些代码来确保这些需求能够并发地执行

要点：
    1）协程是采用 async关键字所定义的函数。如果翔之星这个协程，但并不要求立刻就获得执行结果，而是稍后再来获取，
        那么可以通过 await关键字表达这个意思

    2）协程能够制造出这样一种效果，让人以为程序里有成千上万个函数都在同一时刻高效地运行着

    3）协程可以用 fan-out（分派）与 fan-in（归集）模式实现并行的 I/O操作，而且能够客服用线程做 I/O时的缺陷
"""
import asyncio

from life_game_v1 import Grid, ColumnPrinter

ALIVE = '*'
EMPTY = '-'


async def game_logic(state, neighbors):
    """ 游戏逻辑"""
    if state == ALIVE:
        if neighbors < 2:
            return EMPTY  # Die: too few

        elif neighbors > 3:  # Die: too many
            return EMPTY

    else:
        if neighbors == 3:
            return ALIVE  # Regenerate

    await asyncio.sleep(0.1)

    return state


async def count_neighbors(y, x, get):
    """
        辅助函数，计算相邻单元格的状态
        计算顺序：从北方开始，逆时针转一圈
    """
    n_ = get(y - 1, x + 0)  # North
    ne = get(y - 1, x + 1)  # Northeast
    e_ = get(y + 0, x + 1)  # East
    se = get(y + 1, x + 1)  # Southeast
    s_ = get(y + 1, x + 0)  # South
    sw = get(y + 1, x - 1)  # Southwest
    w_ = get(y + 0, x - 1)  # West
    nw = get(y - 1, x - 1)  # Northwest

    neighbor_states = [n_, ne, e_, se, s_, sw, w_, nw]

    count = 0
    for state in neighbor_states:
        if state == ALIVE:
            count += 1

    await asyncio.sleep(0.1)
    return count


async def step_cell(y, x, get, set):
    state = get(y, x)
    neighbors = await count_neighbors(y, x, get)
    next_state = await game_logic(state, neighbors)
    set(y, x, next_state)


async def simulate(grid):
    """
    一些说明
        1）在调用 step_cell的时候，系统并不会立刻执行这个函数，而是会返回一个协程实例，稍后会把这个实例写在 await表达式里面。
            这里的step_cell，好比那种用 yield写成的生成器一样，调用时并不立刻执行它，而是返回一个生成器实例
            这样就可以实现任务 fan-out（分派）模式了

        2）gather函数可以用来实现 fan-in（归集）模式，把 gather写在 await表达式里面，可以让系统用时间循环去并发地执行那些 step_cell协程，
            并在全部执行完之后，再往下推进 simulate协程

        3）由于这些代码都是在同一线程里执行的，因此不需要给 Grid实例加锁
    """
    next_grid = Grid(grid.height, grid.width)

    tasks = []
    for y in range(grid.height):
        for x in range(grid.width):
            task = step_cell(y, x, grid.get, next_grid.set)
            tasks.append(task)

    await asyncio.gather(*tasks)

    return next_grid


def demo(grid_cls):
    grid = grid_cls(5, 9)
    grid.set(0, 3, ALIVE)
    grid.set(1, 4, ALIVE)
    grid.set(2, 2, ALIVE)
    grid.set(2, 3, ALIVE)
    grid.set(2, 4, ALIVE)

    # print(grid)
    """
    out:
        ---*-----
        ----*----
        --***----
        ---------
        ---------
    """

    columns = ColumnPrinter()

    for i in range(5):
        columns.append(str(grid))
        grid = asyncio.run(simulate(grid))

    print(columns)
    """
    out:
            0     |     1     |     2     |     3     |     4    
        ---*----- | --------- | --------- | --------- | ---------
        ----*---- | --*-*---- | ----*---- | ---*----- | ----*----
        --***---- | ---**---- | --*-*---- | ----**--- | -----*---
        --------- | ---*----- | ---**---- | ---**---- | ---***---
        --------- | --------- | --------- | --------- | ---------
    """


if __name__ == '__main__':
    demo(Grid)
